<?php
/***
 * Candy框架 多线程请求类
 *
 * $Author: 刘森 (fingerboy@qq.com) $
 * $Date: 2019-10-01 01:20:28 $
 */

declare(strict_types=1);
namespace Candy\Extend\Network;

defined('CANDY') OR die('You Are A Bad Guy. o_O???');

final Class CurlMulti {

    /* 请求结果 */
    private $result;

    /* curl对象列表 */
    private $chList = array();

    /* 保存请求参数 */
    private $paramList = array();

    /* 附加参数序号  */
    private $param_index = 0;

    /* 附加参数前缀 */
    private $param_name = "_mpi_";

    function __construct(){
        $this->param_name .= rand(10, 1000000);
    }

    /**
     * 获得请求结果
     * @return multitype:
     */
    public function getResult() {
        return $this->result;
    }

    /**
     * 发起get请求
     * @param unknown $curlData
     */
    public function doGet($curlData) {
        return $this->doCurl($curlData, false);
    }

    /**
     * 发起post请求
     * @param unknown $curlData
     * @return Ambigous <string, void>
     */
    public function doPost($curlData) {
        return $this->doCurl($curlData, true);
    }

    /**
     * 执行批量请求
     * @param unknown $curlData
     * @return string
     */
    private function doCurl($curlData, $isPost) {
        // 初始化校验
        $initSuccess = $this->init($curlData, $isPost);
        if (!$initSuccess || count($this->chList) == 0) {
            if ($this->result == "") {
                $this->result = "初始化失败";
            }
            return;
        }
        // 创建多请求执行对象
        $downloader = curl_multi_init();
        // 将三个待请求对象放入下载器中
        foreach ($this->chList as $ch) {
            curl_multi_add_handle($downloader, $ch);
        }
        // 是否执行成功
        $isSuccess = true;
        $this->result = Array();
        // 轮询执行请求
        while (true) {
            // 遍历等待执行完成
            while (($execrun = curl_multi_exec($downloader, $running)) == CURLM_CALL_MULTI_PERFORM);
            if ($execrun != CURLM_OK) {
                $isSuccess = false;
                $this->result = "请求失败";
                break;
            }

            // 处理完成的请求
            while ($done = curl_multi_info_read($downloader)) {
                // 从请求中获取信息、内容、错误
                $info = curl_getinfo($done['handle']);
                $output = curl_multi_getcontent($done['handle']);
                $error = curl_error($done['handle']);

                $url = $info["url"];
                $tmp_param_index = $this->getLastParam($url, $this->param_name);
                $params = $this->paramList[$tmp_param_index];
                $url = substr($url, 0, strpos($url, $this->param_name) - 1);

                // 保存请求结果
                $this->result[] = Array(
                    "url" => $url,
                    "params" => $params,
                    "response" => $output
                );

                // 清除已完成的请求
                curl_multi_remove_handle($downloader, $done['handle']);
            }

            // 当没有数据的时候进行堵塞，把 CPU 使用权交出来，避免上面死循环空跑数据导致 CPU 100%
            if ($running) {
                $rel = curl_multi_select ( $downloader, 1 );
                if ($rel == - 1) {
                    usleep ( 1000 );
                }
            }

            if ($running == false) {
                break;
            }
        }

        // 请求完毕,关闭下载器
        curl_multi_close($downloader);
        return "批量请求成功";
    }

    /**
     * 批量请求初始化
     * @param unknown $curlData
     * @return boolean
     */
    private function init($curlData, $isPost) {
        foreach ($curlData as $url => $params) {
            if (!$this->isLegalUrl($url)) {
                $this->result = "初始化失败：" . $url. "不是一个合法的地址";
                return false;
            }
            if (!is_array($params)) {
                $this->result = "初始化失败：" . $params. "不是数组";
                return false;
            }
            if (!$isPost) {
                $url = $this->appendParamArray($url, $params);
            }
            $url = $this->appendParam($url, $this->param_name, $this->param_index);
            $this->paramList[] = $params;
            $this->chList[] = $this->initCurlObject($url, $isPost, $params);
            $this->param_index++;
        }
        return true;
    }

    /**
     * 初始化curl对象
     * @param unknown $url
     * @param unknown $paramData
     * @param unknown $header
     * @return resource
     */
    private function initCurlObject($url, $isPost, $paramData = array()) {
        $options = array();
        $url = trim($url);
        $options[CURLOPT_TIMEOUT] = 10;
        $options[CURLOPT_USERAGENT] = self::getAgent();
        $options[CURLOPT_RETURNTRANSFER] = true;
        if ($isPost) {
            $options[CURLOPT_POST] = true;
            if (!empty($paramData) && is_array($paramData)) {
				if(isset($paramData['file'])){
					//上传文件
					foreach($paramData['file'] as $key=>$val){
						if(file_exists($val)){
							$paramData[$key]=new \CURLFile($val);
						}
					} 
					unset($paramData['file']);
					$options[CURLOPT_POSTFIELDS] = $paramData;
				}else{
					$options[CURLOPT_POSTFIELDS] = http_build_query($paramData);
				}
            }
        }
        if (stripos($url, 'https') === 0) {
            $options[CURLOPT_SSL_VERIFYPEER] = false;
        }
        $options[CURLOPT_URL] = $url;
        $ch = curl_init();
        curl_setopt_array($ch, $options);
        return $ch;
    }

    /**
     * 判断url是否合法地址
     * @param unknown $str
     * @return boolean
     */
    private function isLegalUrl($str) {
        return $this->startWith($str, "http://") || $this->startWith($str, "https://");
    }

    /**
     * 判断字符串是否以$part开头
     * @param unknown $str
     * @param unknown $part
     * @return boolean
     */
    private function startWith($str, $part) {
        return strpos($str, $part) === 0;
    }

    /**
     * url后附加单个请求参数
     * @param unknown $url
     * @param unknown $param_name
     * @param unknown $param_val
     * @return string
     */
    private function appendParam($url, $param_name, $param_val) {
        return $url . $this->calcUrlParamSymbol($url) . $param_name . "=" . $param_val;
    }

    /**
     * url后附加多个请求参数
     * @param unknown $url
     * @param unknown $param_array
     * @return unknown|string
     */
    private function appendParamArray($url, $param_array) {
        if (!is_array($param_array) || empty($param_array)) {
            return $url;
        }
        return $url . $this->calcUrlParamSymbol($url) . http_build_query($param_array);
    }

    /**
     * 计算url下一个参数连接符
     * @param unknown $url
     * @return string
     */
    private function calcUrlParamSymbol($url) {
        return strpos($url, "?") === FALSE ? "?" : "&";
    }

    /**
     * 获得url最后一个参数值
     * @param unknown $url
     * @param unknown $param_name
     * @return Ambigous <string, unknown>
     */
    function getLastParam($url, $param_name) {
        return preg_match('/' . $param_name .'=(.*)$/i', $url, $matches) ? $matches[1] : "";
    }

    /**
     * 模拟常用浏览器的useragent
     */
    public function getAgent($type='pc')
    {
        $agentarry['wap'] = [
            'safariiOS4.33–iPhone' => 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
            'safariiOS4.33–iPodTouch' => 'Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
            'safariiOS4.33–iPad' => 'Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5',
            'Android-N1' => 'Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
            'QQ浏览器' => 'MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
            'Opera' => 'Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10',
            'MotoXoom' => 'Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13',
            'BlackBerry' => 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+',
            'HPTouchpad' => 'Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0',
            'NokiaN97' => 'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124',
            'Mango' => 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)',
            'UC无' => 'UCWEB7.0.2.37/28/999',
            'UC标准' => 'NOKIA5700/ UCWEB7.0.2.37/28/999',
            'UCOpenwave' => 'Openwave/ UCWEB7.0.2.37/28/999',
            'UCOpera' => 'Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999',
            '小米' => 'Mozilla/5.0 (Linux; Android 7.1.1; MI 6 Build/NMF26X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/043807 Mobile Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/WIFI Language/zh_CN',
            '锤子OD103型号' => 'Mozilla/5.0 (Linux; Android 7.1.1; OD103 Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043632 Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/4G Language/zh_CN',
            '锤子SM919型号' => 'Mozilla/5.0 (Linux; Android 6.0.1; SM919 Build/MXB48T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043632 Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/WIFI Language/zh_CN',
            'VIVOX6S' => 'Mozilla/5.0 (Linux; Android 5.1.1; vivo X6S A Build/LMY47V; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043632 Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/WIFI Language/zh_CN',
            'HUAWEITAG-AL00' => 'Mozilla/5.0 (Linux; Android 5.1; HUAWEI TAG-AL00 Build/HUAWEITAG-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043622 Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/4G Language/zh_CN',
            '小程序' => 'Mozilla/5.0 (Linux; Android 7.1.1; MI 6 Build/NMF26X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/043807 Mobile Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/4G Language/zh_CN MicroMessenger/6.6.1.1220(0x26060135) NetType/4G Language/zh_CN miniProgram',
            'iphoneOS9' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13F69 MicroMessenger/6.6.1 NetType/4G Language/zh_CN',
            'iphoneOS112' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_2_2 like Mac OS X) AppleWebKit/604.4.7 (KHTML, like Gecko) Mobile/15C202 MicroMessenger/6.6.1 NetType/4G Language/zh_CN',
            'iphoneOS111' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_1 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B150 MicroMessenger/6.6.1 NetType/WIFI Language/zh_CN',
            'iphoneX' => 'Mozilla/5.0 (iphone x Build/MXB48T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043632 Safari/537.36 MicroMessenger/6.6.1.1220(0x26060135) NetType/WIFI Language/zh_CN',
        ];
        $agentarry['pc'] = [
            'safari5.1–MAC' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
            'safari5.1–Windows' => 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
            'IE9.0' => 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;',
            'IE8.0' => 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)',
            'IE7.0' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)',
            'IE6.0' => ' Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
            'Firefox4.0.1–MAC' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv,2.0.1) Gecko/20100101 Firefox/4.0.1',
            'Firefox4.0.1–Windows' => 'Mozilla/5.0 (Windows NT 6.1; rv,2.0.1) Gecko/20100101 Firefox/4.0.1',
            'Opera11.11–MAC' => 'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11',
            'Opera11.11–Windows' => 'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11',
            'Chrome17.0–MAC' => ' Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11',
            'Chrome17.0–Windows' => 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
            '傲游' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)',
            '腾讯TT' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)',
            '世界之窗2.x' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
            '世界之窗3.x' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)',
            '搜狗浏览器1.x' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)',
            '360浏览器' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)',
            'Avant' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)',
            'GreenBrowser' => ' Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
        ];
        return $agentarry[$type][array_rand($agentarry,1)];
	}
}
